#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  circuit.py
#  
#  Copyright 2012 Juan Pablo Caram <jpcaram@gmail.com>
#  
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#  
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#  
#  

from subprocess import Popen, PIPE
import os
from numpy import array, hstack, vstack, transpose,column_stack
from matplotlib.pyplot import plot, xlim, ylim, xlabel, ylabel, grid
from copy import deepcopy
import string
import random

#########################################
#				UTILITIES				#
#########################################
def myFind(thelist, cond, param):
	n = 0
	for item in thelist:
		if cond(item, param):
			return [n, item]
		n += 1
	return None
	
def isComponentLine(line, name):
	parts = line.split()
	if len(parts) > 0:
		if parts[0] == name:
			return True
	return False

def sch2net(src, dst, opt=None):
	os.system("gnetlist -g spice-sdb -o " + dst + " " + src)

def arrayFromFile(filename, headerRow, delimiter=","):
	data = []
	f = open(filename,  "r")
	lineNumber = 0
	numCols = -1
	for line in f:
		lineNumber += 1
		if lineNumber == headerRow:
			parts = line.split(delimiter)
			numCols = len(parts)
		if lineNumber <= headerRow:
			continue
		parts = line.split(delimiter)
		if len(parts) != numCols:
			break
		numericalParts = []
		for part in parts:
			numericalParts.append(float(part))
		data.append(numericalParts)
	return array(data)
		
def eng2float(number):
	engNot = {
		'k':'e3',
		'K':'e3',
		'M':'e6',
		'G':'e9',
		'T':'e12',
		'P':'e15',
		'm':'e-3',
		'u':'e-6',
		'U':'e-6',
		'N':'e-9',
		'n':'e-9',
		'p':'e-12',
		'F':'e-15',
		'f':'e-15',
		'A':'e-18',
		'a':'e-18'
		}
	if type(number) is str:
		number = number.replace("Meg", "e6")	# Spice
		for key in engNot.keys():
			number = number.replace(key, engNot[key])
	return float(number)

def randString(size=10, chars=string.ascii_uppercase + string.digits):
	return ''.join(random.choice(chars) for x in range(size))
#########################################
#				MAIN CLASS				#
#########################################
class Circuit:
	def __init__(self, 
				name="newcircuit", 
				schematic=None, 
				netlist=None, 
				simulator="ngspice"):
		
		#self.simulationDataFile = None
		#self.spiceMainFile = None
		#self.simulationData = SimData()
		
		# Netlist object representinc the circuit
		self.netlist = None
		
		# Name for convenience and file names
		self.name = name
		
		# Original schematic file from which this object was created if any
		self.schematicFileName = schematic
		
		# Original netlist file from which this object was created if any
		self.netlistFile = netlist
		
		# Simulator object
		if simulator == "ngspice":
			self.simulator = NGSpice()
		elif simulator == "gnucap":
			self.simulator = GnuCap()
		else:
			print "ERROR: Specified simulator not supported: " + simulator
			return None
		
		# Create netlist object from schematic or netlist text
		self.netlist = Netlist()
		if self.schematicFileName != None and self.netlistFile == None:
			self.netlistFile = self.schematicFileName + ".net"
			sch2net(self.schematicFileName, self.netlistFile)
			self.netlist.getFromFile(self.netlistFile)
		else:
			if self.netlistFile != None:
				self.netlist.getFromFile(self.schematicFileName)
	
	def resetNetlist(self):
		'''
		Re-creates the netlist from the schematics file.
		'''
		sch2net(self.schematicFileName, self.netlistFile)
		self.netlist.getFromFile(self.netlistFile)
	
	def runTransient(self, varList, step, stop, start=None, verbose=False):
		'''
		Shortcut to simulator object's method within circuit object.
		'''
		return self.simulator.runTransient(self, varList, step, stop, start, verbose=verbose)
		
	def runAC(self, varList , step, stop, start=1, stepType="dec", verbose=False):
		'''
		Shortcut to simulator object's method within circuit object.
		'''
		return self.simulator.runAC(self, varList , step, stop, start=1, stepType='dec', verbose=verbose)
		
	def runDC(self, varList , sweepSpecs, verbose=False):
		'''
		Shortcut to simulator object's method within circuit object.
		'''
		return self.simulator.runDC(self, varList , sweepSpecs, verbose=verbose)
		
	def runSpectrum(self, varList , step, stop, start=1, verbose=False):
		'''
		Shortcut to simulator object's method within circuit object.
		'''
		return self.simulator.runSpectrum(self, varList , step, stop, start, verbose=verbose)
		
	"""
	def simNetlist(self, verbose=False):
		'''
		Simulates the circuit's netlist and returns a SimData()
		object with the results.
		'''
		# Store the netlist in a file
		self.netlist.writeToFile(self.name + '.net')
		
		# Run spice with netlist
		gnucapProc = Popen(["gnucap", "-b", self.name+".net"], stdout=PIPE)
		result = str(gnucapProc.communicate()[0])
		if verbose:
			print result
			
		# Create simulation data set from spice outout
		self.simulationData.createFromString(result)
		#self.simResultSets.append(sim)
		return self.simulationData
	
	def simNetlistNGSpice(self, verbose=False):
		# Store the netlist in a file
		self.netlist.writeToFile(self.name + '.net')
		
		# Run spice with netlist
		ngspiceProc = Popen(["ngspice", "-n", "-b", self.name+".net"], stdout=PIPE)
		
		result = str(ngspiceProc.communicate()[0])
		if verbose:
			print result
		
		# Create simulation data set from spice output
		self.simulationData.createFromNGSpiceString(result)
		
		return self.simulationData
		
	def simulate(self, verbose=False):
		if self.simulator == "ngspice":
			self.simNetlistNGSpice(verbose=verbose)
		else:
			self.simNetlist(verbose=verbose)
	"""
		
		
#########################################
#				NETLIST					#
#########################################
class Netlist:
	def __init__(self):
		# The text defining the spice netlist. It's a list
		# and each element is a line of text (string).
		self.rawText = None
		
	def getFromFile(self, filename):
		'''
		Reads a spice netlist into self.rawText
		'''
		f = open(filename, "r")
		self.rawText = []
		for line in f:
			self.rawText.append(line.replace('\n',''))
	
	def changeValue(self, name, value):
		'''
		Changes the value of a component in the netlist.
		Example:
		 c.changeValue("C1", "12p")
		 This sets the value of C1 to 12pF
		'''
		if name[0] == 'M':
			validx = 5
		else:
			validx = 3
		
		x = myFind(self.rawText, isComponentLine, name)
		index = x[0]
		line = x[1]
		parts = line.split()
		
		newline = name
		for i in range(1,validx):
			newline += " " + parts[i]
		#lastIndex = len(parts)-1
		value = str(value)
		newline += " " + value
		self.rawText[index] = newline
		
	def writeToFile(self, filename):
		'''
		Write the netlist text to file.
		'''
		f = open(filename, "w")
		for line in self.rawText:
			f.write(line + "\n")
			
	def changeParam(self, param, repl=None):
		'''
		Changes a spice parameter in the netlist.
		Example:
		 c.changeParam(".TR 0.00m 1.00u 0.10n TE 27.00 BASIC")
		 Finds the line beginning with .TR and replaces the
		 whole line with the given string.
		'''
		parts = param.split()
		x = myFind(self.rawText, isComponentLine, parts[0])
		index = x[0]
		if len(x) < 1:
			return False
		if repl == None:
			self.rawText[index] = param
		else:
			self.rawText[index] = repl
			return True
		
	def appendText(self, text):
		for line in text:
			self.rawText.append(line)
		if self.changeParam(".end", ""):
			self.rawText.append(".end")
		else:
			self.changeParam(".END", "")
			self.rawText.append(".end")
	
	def healthy(self):
		return

#########################################
#				SIMDATA					#
#########################################
class SimData(dict):
	def __init__(self):
		self.type = None
		super(dict, self).__init__()
		
		# names of variables in the file
		#self.header = None
		
		# numerical values of the variables. It's a list while it's
		# being populated, and converted to array at the end, for
		# efficiency.
		#self.data = []
					
	def getVar(self, var):
		'''
		Returns a vector with all the values computed
		for the variable var. OBSOLETE. Use keys instead. This
		class now extends dict.
		'''
		#index = self.header.index(var)
		#return [row[index] for row in self.data]
		#return self.data[:,index]
		return self[var]
		
	def plot(self, xname, yname, xlimits=None, 
								ylimits=None, 
								xlabel_=None, 
								ylabel_=None,
								xscale=1,
								yscale=1):
		'''
		TODO: Make a Plotable Class and extend it.
		'''
		#x = xscale*array(self.getVar(xname))
		#y = yscale*array(self.getVar(yname))
		x = xscale*self[xname]
		y = yscale*self[yname]
		plot(x,y)
		if xlimits != None:
			xlim(xlimits)
		if ylimits != None:
			ylim(ylimits)
		if xlabel_ != None:
			xlabel(xlabel_)
		if ylabel_ != None:
			ylabel(ylabel_)
		grid(True)
		
	def doDSP(self, varList, dspObj):
		fulldata = None
		for avar in varList:
			x = self[avar]
			if fulldata == None:
				fulldata = x
			else:
				fulldata = column_stack((fulldata, x))
		dspObj.auto(fulldata)
		return dspObj
		
	def getCursor(self, var, value):
		lastIndex = None
		cursor = Cursor(self)
		cursor.xName = var
		
		# iterate over samples
		for index in range(len(self[var])):
			currentVal = self[var][index]
			
			# Until the desired value lies between the previous
			# and current samples
			if lastIndex == None:
				lastIndex = index
				continue
			if (self[var][index] <= value and self[var][lastIndex] > value) or (self[var][index] >= value and self[var][lastIndex] < value):
				currentDelta = abs(self[var][index] - value)
				lastDelta = abs(self[var][lastIndex] - value)
				if currentDelta < lastDelta:
					cursor.closestIndex = index
					return cursor
				else:
					cursor.closestIndex = lastIndex
					return cursor
			
		
class Cursor:
	def __init__(self, data):
		self.data = data
		self.closestIndex = None
		self.xName = None
		
	def delta(cursor1, cursor2, var):
		a = cursor2.data[var][cursor2.closestIndex]
		b = cursor1.data[var][cursor1.closestIndex]
		return a - b
		
	def slope(cursor1, cursor2, xvar, yvar):
		dx = Cursor.delta(cursor1, cursor2, xvar)
		dy = Cursor.delta(cursor1, cursor2, yvar)
		if dx == 0:
			print "ERROR: dx=0"
			return None
		else:
			return dy/dx
			
	def value(self, var):
		return self.data[var][self.closestIndex]
		
		
#########################################
#			CIRCUIT TEST				#
#########################################
class circuitTest:
	def __init__(self, circuit):
		self.circuit = circuit
		self.simulationDefinition = ""
		self.parameters = []
		self.metrics = []
		self.results = []
		self.originalNetlist = deepcopy(self.circuit.netlist)
		
	def setSimDefinition(self, simDef):
		'''
		Defines how the simulation will be executed for this test.
		For now, just a spice directive in plain text.
		'''
		self.simulationDefinition = simDef
		
	def addParameter(self, param):
		self.parameters.append(param)
		
	def addMetric(self, metric):
		self.metrics.append(metric)
		
	
	def createValueSetDictionary(self):
		'''
		Creates a dictionary who's keys are the names of the parameters
		in this test and values are set to None.
		'''
		vd = {}
		for param in self.parameters:
			vd[param.name] = None
		return vd
	
	def runOnce(self, params):
		# Reset the netlist to original
		self.circuit.netlist = deepcopy(self.originalNetlist)
		
		# Set parameters into the netlist
		for key in params.keys():
			#print key+"="+str(params[key])+", ",
			
			self.circuit.netlist.changeValue(key, params[key])
			#print str(myFind(self.circuit.netlist.rawText, isComponentLine, key))+"/",
		#print "simulating"
		
		# Simulate
		#self.circuit.resetNetlist()
		self.circuit.netlist.appendText(self.simulationDefinition)
		self.circuit.simulate()
		
		# Compute metrics
		for metric in self.metrics:
			# get the needed data
			data = None
			for colName in metric.datanames:
				if data == None:
					data = self.circuit.simulationData.getVar(colName)
				else:
					data = column_stack((data, self.circuit.simulationData.getVar(colName)))
			# Run the DSP

			metric.dspobj.__init__()	# Reset the DSP obj
			metric.dspobj.auto(data)
			
		# Save results. Add a row ro results. One column per parameters
		# and one column per metric.
		results = []
		for param in self.parameters:
			results.append(params[param.name])
		for metric in self.metrics:
			results.append(metric.dspobj.result)
		self.results.append(results)
		
	
	def run(self, valueSet=None):
		'''
		Executes the test.
		'''
		# First time
		if valueSet == None:
			valueSet = self.createValueSetDictionary()
			
		# Find a variable which has not been set
		unsetVarKeys = [key for key in valueSet.keys() if valueSet[key] == None]
		
		# All values have been set. Run the simulation
		# End of recursion
		if len(unsetVarKeys) == 0:
			self.runOnce(valueSet)
			return
			
		# Variable not set yet. Set all its possible values
		# and continue setting variables
		varKey = unsetVarKeys[0]
		param = [p for p in self.parameters if p.name == varKey][0]
		for value in param.values:
			valueSet[varKey] = value
			# Recursion
			self.run(valueSet)
			valueSet[varKey] = None
			
	def report(self, printSpec):
		# Index variable for each iteration 
		col = 0
		
		# Title row for report
		title = ""
		
		# List of the nominal values for all parameters in the test
		nomParams = []
		
		# Flag marking if a row in the results is the nominal result
		isNominal = True
		
		# Nominal result of the test. List of parameter and metric values.
		nominalResult = None
		
		# find nominal metric
		for param in self.parameters:
			nomParams.append(param.nominal)
		
		for result in self.results:
			for i in range(len(nomParams)):
				isNominal = isNominal and result[i] == nomParams[i]
			if isNominal:
				nominalResult = result
			isNominal = True
		
		# Create/print title
		for param in self.parameters:
			title += "%*s"%(printSpec[col][0] + printSpec[col][1] + 4, param.name)
			col += 1
		for metric in self.metrics:
			title += "%*s"%(printSpec[col][0] + printSpec[col][1] + 4, metric.name)
			title += " (100.00%)"
			col += 1
		print title
		
		# Print data
		col = 0
		for row in self.results:
			textRow = ""
			for val in row:
				textRow += "   %*.*f"%(printSpec[col][0], printSpec[col][1], val)
				if col >= len(self.parameters):	# Metric
					textRow += " (% 3.2f%%)"%(100.0*(val-nominalResult[col])/nominalResult[col])
				col += 1
			print textRow
			col = 0
		
class circuitParameter:
	def __init__(self, name, nominal=None, values=[]):
		self.name = name
		self.nominal = nominal
		self.values = values
		
class circuitMetric:
	def __init__(self, name, dspobj=None, datanames=[]):
		self.name = name
		self.dspobj = dspobj
		self.datanames = datanames
	
#########################################
#			DSP FUNCTIONS				#
#########################################
class dsp(object):
	def __init__(self):
		self.start = 0
		self.stop = None
		return
		
	def processSample(self, sample, addtnl = None):
		return
		
	def finalize(self):
		return
		
	def auto(self, data):
		'''
		Iterates over data, passing each element to
		self.processSample
		'''
		for row in data:
			self.processSample(row)
		
		self.finalize()
			
	def plot(self, xlimits=None, 
					ylimits=None, 
					xlabel_=None, 
					ylabel_=None,
					xscale=1,
					yscale=1):
		'''
		Plots the first 2 columns of self.output agains each other.
		'''
		output = array(self.output)
		x = xscale*output[:,0]
		y = yscale*output[:,1]
		plot(x,y)
		if xlimits != None:
			xlim(xlimits)
		if ylimits != None:
			ylim(ylimits)
		if xlabel_ != None:
			xlabel(xlabel_)
		if ylabel_ != None:
			ylabel(ylabel_)
		grid(True)
	

			
class Average(dsp):
	def __init__(self):
		super(Average, self).__init__()
		self.integral = 0
		self.result = 0
		self.fifo = None
		
	def processSample(self, sample):
		x = sample[0]
		y = sample[1]
		if self.fifo != None:
			# Trapezoidal integration
			self.integral = self.integral \
				+ (x - self.fifo[0]) * (y + self.fifo[1]) / 2
		self.fifo = [x, y]
		
	def finalize(self):
		try:
			self.result = self.integral / self.fifo[0]
		except:
			self.result = 0
			
class Rms(dsp):
	def __init__(self):
		super(Rms, self).__init__()
		self.integral = 0
		self.result = 0
		self.output = []
		self.fifo = None
		
	def processSample(self, sample):
		x = sample[0]
		y = sample[1]
		
		if self.fifo != None:
			# Trapezoidal integration
			self.integral = self.integral + \
				(x - self.fifo[0]) * (y**2 + self.fifo[1]**2) / 2
		self.fifo = [x, y]
		if x == 0:
			self.output.append([x, 0])
		else:
			self.output.append([x, (self.integral / x)**0.5])
		
	def finalize(self):
		try:
			# sqrt(1/T * Integral)
			self.result = (self.integral / self.fifo[0])**0.5
		except:
			self.result = 0
		self.output = array(self.output)
	
class Max(dsp):
	def __init__(self):
		super(Max, self).__init__()
		self.result = None
		#self.fullsamplemax = None
		
	def processSample(self, sample):
		if self.result == None or self.result < sample:
			self.result = sample
			#self.fullsamplemax = fullsample	

class MaxAt(dsp):
	'''
	From a sequence [x[n], y[n]] obtains x[k] for which y[k]
	is maximum.
	'''
	def __init__(self):
		super(MaxAt, self).__init__()
		self.maxval = None
		self.result = None
		
	def processSample(self, sample):
		'''
		sample has [x, y] format.
		'''
		if self.result == None or self.maxval < sample[1]:
			self.maxval = sample[1]
			self.result = sample[0]

class Min(dsp):
	def __init__(self):
		super(Min, self).__init__()
		self.result = None
		self.fullsamplemin = None
		
	def processSample(self, sample, fullsample=None):
		if self.result == None or self.result > sample:
			self.result = sample
			self.fullsamplemin = fullsample	

class Pk2pk(dsp):
	def __init__(self):
		super(Pk2pk, self).__init__()
		self.mymax = Max()
		self.mymin = Min()
		self.result = 0
		
	def processSample(self, sample):	
		self.mymax.processSample(sample)
		self.mymin.processSample(sample)
		try:
			self.result = self.mymax.result - self.mymin.result
		except:
			pass

class Der(dsp):
	def __init__(self):
		super(Der, self).__init__()
		self.lastx = None
		self.lasty = None
		self.output = []
		self.result = None
		
	def processSample(self, sample):
		x = sample[0]
		y = sample[1]
		if self.lastx == None:
			self.lastx = x
			self.lasty = y
			self.output.append([x, 0])
			return
		dx = x - self.lastx
		dy = y - self.lasty
		self.lastx = x
		self.lasty = y
		if dx == 0:
			self.output.append([x, 0])
			return
		self.output.append([x, dy/dx])
		
	def finalize(self):
		self.output = array(self.output)

#########################################
#			SIMULATOR CLASSES			#
#########################################
class Simulator(object):
	def __init__(self):
		# A string that identifies the simulator
		self.name = None
		
		# Path to the executable
		self.path = None
		
		# List of argument for the executable to be passed
		# to Popen
		self.args = []
		
		# Name for temporary files
		self.unique = randString()
		
		# Results parser
		simParser = None
		
		# Default simulation options
		self.defaultOpts = ".OPTIONS"
		
	def simulateFile(self, filename, output):
		'''
		Runs the simulator executable upon a file.
		Returns the output as a string.
		'''
		args = deepcopy(self.args)
		args.append(filename)
		spiceProc = Popen(args, stdout=PIPE)
		result = str(spiceProc.communicate()[0])
		return result
		
	def simulateFile(self, filename):
		'''
		Runs the simulator executable over the given file.
		
		RETURNS {Array(String)} Raw output from the simulator
		'''
		# Run spice with netlist
		args =  deepcopy(self.args)
		args.append(filename)
		gnucapProc = Popen(args, stdout=PIPE)
		result = str(gnucapProc.communicate()[0])
		
		return result
		
	def simulateFullNetlist(self, netlist, keepFile=False, verbose=False):
		'''
		Runs the simulator with a given netlist. This netlist must
		include all simulation instructions as it will be passed
		directly to the simulator executable.
		
		RETURNS {SimData} Parsed simulation data
		'''
		tempFilename = self.unique + ".net"
		
		# Write file
		netlist.writeToFile(tempFilename)
		
		# Simulate
		result = self.simulateFile(tempFilename)
		
		# Print output if verbose output requested
		if verbose:
			print result
		
		# Remove temp file
		if keepFile == False:
			os.system("rm " + tempFilename)
		
		# Parse output
		parser = self.simParser()
		parser.parse(result)
		
		# Return SimData generated by the parser
		return parser.data
		
	def runTransient(self, circuit, varList, step, stop, start=None, verbose=False):
		# Update netlist with simulation commands
		netlist = deepcopy(circuit.netlist)
		xtras = [self.defaultOpts, ".PRINT TRAN " + " ".join(varList), 
				 self.transientDirective(step, stop, start)]
		netlist.appendText(xtras)
		
		# Send out for simulation. Returns a SimData object with the
		# parsed results.
		return self.simulateFullNetlist(netlist, verbose=verbose)
		
	def runAC(self, circuit, varList , step, stop, start=1, stepType="dec", verbose=False):
		# Update netlist with simulation commands
		netlist = deepcopy(circuit.netlist)
		xtras = [self.defaultOpts, ".PRINT AC " + " ".join(varList), 
				 self.acDirective(step, stop, start, stepType)]
		netlist.appendText(xtras)
		
		# Send out for simulation. Returns a SimData object with the
		# parsed results.
		return self.simulateFullNetlist(netlist, verbose=verbose)
		
	def runDC(self, circuit, varList , sweepSpecs, verbose=False):
		# Update netlist with simulation commands
		netlist = deepcopy(circuit.netlist)
		xtras = [self.defaultOpts, ".PRINT DC " + " ".join(varList), 
				 self.dcDirective(sweepSpecs)]
		netlist.appendText(xtras)
		
		# Send out for simulation. Returns a SimData object with the
		# parsed results.
		return self.simulateFullNetlist(netlist, verbose=verbose)
		
	def runSpectrum(self, circuit, varList , step, stop, start=1, verbose=False):
		# Update netlist with simulation commands
		netlist = deepcopy(circuit.netlist)
		xtras = [self.defaultOpts, ".PRINT FOURIER " + " ".join(varList), 
				 self.spectrumDirective(step, stop, start)]
		netlist.appendText(xtras)
		
		# Send out for simulation. Returns a SimData object with the
		# parsed results.
		return self.simulateFullNetlist(netlist, verbose=verbose)
		
class NGSpice(Simulator):
	'''
	1) Relevant command line options:
		-n do not load .spiceinit (obsolete)
		-b batch mode (can take stdin?)
		-s server mode (?)
		
	2) Output file format
		2.1) See below....
		No. of Data Rows : 31
					  * gnetlist -g spice-sdb -o csdemo.sch.net csdemo.sch
					  AC Analysis  Sun Mar 18 20:48:07  2012
		----------------------------------------------------------------
		Index   frequency       vp(out)         vp(out)         
		----------------------------------------------------------------
		0	1.000000e+00	3.141278e+00	3.141278e+00	
		1	1.258925e+00	3.141197e+00	3.141197e+00	
		2	1.584893e+00	3.141095e+00	3.141095e+00
		
		2.2) Can contain multiple tables when many variables are generated	
	'''
	def __init__(self):
		super(NGSpice, self).__init__()
		
		self.name = "NG-Spice"
		self.path = "ngspice"
		self.args = [self.path, "-n", "-b"]
		
		# Class, not instance
		self.simParser = NGsimParser
		
		# Default simulation options
		self.defaultOpts = ".OPTIONS NOACCT NOPAGE NOMOD"
	
	def transientDirective(self, step, stop, start=None):
		'''
		Constructs and returns the string instructing NGSpice to
		carry out a TRANSIENT analysis.
		'''
		if start == None:
			start = ""
		return ".tran %s %s %s"%(str(step), 
								str(stop), 
								str(start))
								
	def acDirective(self, step, stop, start=1, stepType="dec"):
		'''
		Constructs and returns the string instructing NGSpice to
		carry out an AC analysis.
		'''
		return ".ac %s %s %s %s"%(str(stepType), 
								str(step), 
								str(start), 
								str(stop))
								
	def dcDirective(self, sweepSpecs):
		'''
		Constructs and returns the string instructing NGSpice to
		carry out a DC analysis.
		
		sweepSpecs is a list, of which each element is a list specifying
		what and how to sweep a variable. Each sweepSpec has the form
		[variable-name, step, stop, start]
		'''
		directive = ".dc"
		for sweep in sweepSpecs:
			source = sweep[0]
			step = sweep[1]
			stop = sweep[2]
			start = sweep[3]
			directive += " %s %s %s %s"%(source, str(start), str(stop),
										 str(step))
		return directive
		

class SimParser(object):
	'''
	Parses the output of a simulation into a SimData() object.
	This class has to be extended for a particular simulator.
	
	Note: Incorrect parsing will ocurr if the simulator prints the
	same variable more than once.
	'''
	def __init__(self):
		# Current header. Names of variables processed at this time.
		self.header = None
		
		# Each key is the name of the variable recorded by the simu-
		# lator. Each element is a 1-D array.
		# Note: SimData is an exended dictionary.
		self.data = SimData()
		
		# State of the state machine processing the text.
		# INIT, HEADER FOUND, COMPLETE
		self.state = "INIT"
		
	def parse(self, rawData):
		'''
		Processes the whole simulation output. rawData a single array
		'''
		rawData = rawData.split("\n")
		
		for line in rawData:
			self.processLine(line)
		
		# Change state
		self.state = "COMPLETE"
		
		if not self.finalize():
			print "ERROR: Variables have different number of samples."
			print "Possible cause: variables printed more than once in simulation."
	
	def finalize(self):
		'''
		To be run after all data lines have been processed.
		'''
		healthy = True
		numitems = None
		
		# Convert data into array and check lengths
		for key in self.data.keys():
			if numitems == None:
				numitems = len(self.data[key])
			else:
				healthy = healthy and (numitems == len(self.data[key]))
			self.data[key] = array(self.data[key])
			
		return healthy
		
	def isData(self, line):
		'''
		Data must have the same number of elements as the header. It
		will only be found after a header has been found.
		'''
		if self.state == "INIT":
			return False
		if self.state == "HEADER FOUND":
			parts = line.split()
			if len(parts) != len(self.header):
				return False
			return True
		return False
	
	def processLine(self, line):
		'''
		Hanles a single line of text from a simulation output.
		Determines how to process it and passes it to the corresponding
		processing method.
		'''
		if self.isHeader(line):
			self.processHeader(line)
			return
		if self.isData(line):
			self.processData(line)
			return
			
	def processHeader(self, headerLine):
		'''
		Extract variable names from a header line and prepares
		for data extraction.
		'''
		
		# Split
		self.header = headerLine.split()
		
		# Add new variables to data
		for i in range(len(self.header)):
			if self.header[i] not in self.data:
				self.data[self.header[i]] = []
				
		# Change state
		self.state = "HEADER FOUND"
		
	def processData(self, dataLine):
		'''
		Extracts data from line, converts it to float and
		stores it appropriately.
		'''
		
		# Split
		parts = dataLine.split()
		
		# Convert
		try:
			for i in range(len(parts)):
				parts[i] = float(parts[i])
		except:
			print "ERROR: Cannot convert to float. Input was:"
			print dataLine
			return
			
		# Store
		for i in range(len(parts)):
			self.data[self.header[i]].append(parts[i])
		
class NGsimParser(SimParser):
	def __init__(self):
		self.rows = 0
		super(NGsimParser, self).__init__()
	
	def isHeader(self, line):
		'''
		The first column is always the sample index number and
		is labelled "Index". A second column always exists.
		Returns True or False
		'''

		parts = line.split()
		if len(parts) < 2:
			return False
		if parts[0] == "Index":
			return True
		return False
		
	def isNumDataRows(self, line):
		if line.find("No. of Data Rows :") == -1:
			return False
		return True
		
	def processLine(self, line):
		# Try to find number of total data rows. Then proceed normally.
		if self.isNumDataRows(line):
			parts = line.split(":")
			self.rows = int(parts[1])
		else:
			super(NGsimParser, self).processLine(line)
			
	def processHeader(self, headerLine):
		# Pre-allocate the data arrays
		super(NGsimParser, self).processHeader(headerLine)
		
		for key in self.data.keys():
			if len(self.data[key]) == 0:
				self.data[key] = [None]*self.rows
				
	def processData(self, dataLine):
		'''
		Extracts data from line, converts it to float and
		stores it appropriately.
		'''
		
		# Split
		parts = dataLine.split()
		
		# Convert
		try:
			for i in range(len(parts)):
				parts[i] = float(parts[i])
		except:
			print "ERROR: Cannot convert to float. Input was:"
			print dataLine
			return
			
		# Store in the right index
		for i in range(len(parts)):
			index = int(parts[0])
			self.data[self.header[i]][index] = parts[i]
		
class GnuCap(Simulator):
	def __init__(self):
		super(GnuCap, self).__init__()
		
		self.name = "GNUCap"
		self.path = "gnucap"
		self.args = [self.path, "-b"]
		
		# Class, not object
		self.simParser = GnuCapSimParser
		
		self.defaultOpts = ".OPTIONS NOPAGE"
	
	def transientDirective(self, step, stop, start=None):
		if start == None:
			start = 0
		return ".transient %s %s %s"%(step, stop, start)
	
	def acDirective(self, step, stop, start=1, stepType="dec"):	
		if stepType == "dec":
			stepType == "decade %s"%(str(step))
			step = ""
		if stepType == "oct":
			stepType == "Octave %s"%(strr(step))
			step = ""
		if stepType == "lin":
			stepType == ""
			
		return ".ac %s %s %s %s"%(str(start), str(stop), str(step), str(stepType)) 
	
	def dcDirective(self, sweepSpecs):
		'''
		sweepSpecs is a list, of which each element is a list specifying
		what and how to sweep a variable. Each sweepSpec has the form
		[variable-name, step, stop, start]
		'''
		directive = ".dc"
		for sweep in sweepSpecs:
			source = sweep[0]
			step = sweep[1]
			stop = sweep[2]
			start = sweep[3]
			directive += " %s %s %s %s"%(source, str(start), str(stop),
										 str(step))
		return directive
		
	def spectrumDirective(self, step, stop, start=1):
		return ".fourier %s %s %s"%(step, stop, start)
		
class GnuCapSimParser(SimParser):
	def isHeader(self, line):
		'''
		Header lines start with a '#'
		'''
		if len(line) < 1:
			return False
		if line[0] != "#":
			return False
		line = line[1:]
		parts = line.split()
		if len(parts) < 2:
			print "Warning: Strange GnuCap header found:"
			print line 
			return False
		return True
		
	def processHeader(self, headerLine):
		# Remove '#' and process normally
		headerLine = headerLine[1:]
		super(GnuCapSimParser, self).processHeader(headerLine)
	
	def processData(self, dataLine):
		'''
		Extracts data from line, converts it to float and
		stores it appropriately.
		
		GnuCap specific: Numbers can be in engineering notation
		e.g. 12k, 6n, etc. This is handled here.
		'''
		# Split
		parts = dataLine.split()
		
		# Convert
		try:
			for i in range(len(parts)):
				parts[i] = eng2float(parts[i])
		except:
			print "ERROR: Cannot convert to float. Input was:"
			print dataLine
			return
			
		# Store
		for i in range(len(parts)):
			self.data[self.header[i]].append(parts[i])

		
# END OF LIBRARY
def main():
	return 0

if __name__ == '__main__':
	main()

